<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_View
 * @subpackage Helper
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Links.php 16971 2009-07-22 18:05:45Z mikaelkael $
 */

/**
 * @see Zend_View_Helper_Navigation_HelperAbstract
 */
require_once 'Zend/View/Helper/Navigation/HelperAbstract.php';

/**
 * Helper for printing <link> elements
 *
 * @category   Zend
 * @package    Zend_View
 * @subpackage Helper
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_View_Helper_Navigation_Links
extends Zend_View_Helper_Navigation_HelperAbstract
{
	/**#@+
	 * Constants used for specifying which link types to find and render
	 *
	 * @var int
	 */
	const RENDER_ALTERNATE  = 0x0001;
	const RENDER_STYLESHEET = 0x0002;
	const RENDER_START      = 0x0004;
	const RENDER_NEXT       = 0x0008;
	const RENDER_PREV       = 0x0010;
	const RENDER_CONTENTS   = 0x0020;
	const RENDER_INDEX      = 0x0040;
	const RENDER_GLOSSARY   = 0x0080;
	const RENDER_COPYRIGHT  = 0x0100;
	const RENDER_CHAPTER    = 0x0200;
	const RENDER_SECTION    = 0x0400;
	const RENDER_SUBSECTION = 0x0800;
	const RENDER_APPENDIX   = 0x1000;
	const RENDER_HELP       = 0x2000;
	const RENDER_BOOKMARK   = 0x4000;
	const RENDER_CUSTOM     = 0x8000;
	const RENDER_ALL        = 0xffff;
	/**#@+**/

	/**
	 * Maps render constants to W3C link types
	 *
	 * @var array
	 */
	protected static $_RELATIONS = array(
	self::RENDER_ALTERNATE  => 'alternate',
	self::RENDER_STYLESHEET => 'stylesheet',
	self::RENDER_START      => 'start',
	self::RENDER_NEXT       => 'next',
	self::RENDER_PREV       => 'prev',
	self::RENDER_CONTENTS   => 'contents',
	self::RENDER_INDEX      => 'index',
	self::RENDER_GLOSSARY   => 'glossary',
	self::RENDER_COPYRIGHT  => 'copyright',
	self::RENDER_CHAPTER    => 'chapter',
	self::RENDER_SECTION    => 'section',
	self::RENDER_SUBSECTION => 'subsection',
	self::RENDER_APPENDIX   => 'appendix',
	self::RENDER_HELP       => 'help',
	self::RENDER_BOOKMARK   => 'bookmark'
	);

	/**
	 * The helper's render flag
	 *
	 * @see render()
	 * @see setRenderFlag()
	 * @var int
	 */
	protected $_renderFlag = self::RENDER_ALL;

	/**
	 * Root container
	 *
	 * Used for preventing methods to traverse above the container given to
	 * the {@link render()} method.
	 *
	 * @see _findRoot()
	 *
	 * @var Zend_Navigation_Container
	 */
	protected $_root;

	/**
	 * View helper entry point:
	 * Retrieves helper and optionally sets container to operate on
	 *
	 * @param  Zend_Navigation_Container $container  [optional] container to
	 *                                               operate on
	 * @return Zend_View_Helper_Navigation_Links     fluent interface, returns
	 *                                               self
	 */
	public function links(Zend_Navigation_Container $container = null)
	{
		if (null !== $container) {
			$this->setContainer($container);
		}

		return $this;
	}

	/**
	 * Magic overload: Proxy calls to {@link findRelation()} or container
	 *
	 * Examples of finder calls:
	 * <code>
	 * // METHOD                  // SAME AS
	 * $h->findRelNext($page);    // $h->findRelation($page, 'rel', 'next')
	 * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section');
	 * $h->findRelFoo($page);     // $h->findRelation($page, 'rel', 'foo');
	 * </code>
	 *
	 * @param  string $method             method name
	 * @param  array  $arguments          method arguments
	 * @throws Zend_Navigation_Exception  if method does not exist in container
	 */
	public function __call($method, array $arguments = array())
	{
		if (@preg_match('/find(Rel|Rev)(.+)/', $method, $match)) {
			return $this->findRelation($arguments[0],
			strtolower($match[1]),
			strtolower($match[2]));
		}

		return parent::__call($method, $arguments);
	}

	// Accessors:

	/**
	 * Sets the helper's render flag
	 *
	 * The helper uses the bitwise '&' operator against the hex values of the
	 * render constants. This means that the flag can is "bitwised" value of
	 * the render constants. Examples:
	 * <code>
	 * // render all links except glossary
	 * $flag = Zend_View_Helper_Navigation_Links:RENDER_ALL ^
	 *         Zend_View_Helper_Navigation_Links:RENDER_GLOSSARY;
	 * $helper->setRenderFlag($flag);
	 *
	 * // render only chapters and sections
	 * $flag = Zend_View_Helper_Navigation_Links:RENDER_CHAPTER |
	 *         Zend_View_Helper_Navigation_Links:RENDER_SECTION;
	 * $helper->setRenderFlag($flag);
	 *
	 * // render only relations that are not native W3C relations
	 * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_CUSTOM);
	 *
	 * // render all relations (default)
	 * $helper->setRenderFlag(Zend_View_Helper_Navigation_Links:RENDER_ALL);
	 * </code>
	 *
	 * Note that custom relations can also be rendered directly using the
	 * {@link renderLink()} method.
	 *
	 * @param  int $renderFlag                    render flag
	 * @return Zend_View_Helper_Navigation_Links  fluent interface, returns self
	 */
	public function setRenderFlag($renderFlag)
	{
		$this->_renderFlag = (int) $renderFlag;
		return $this;
	}

	/**
	 * Returns the helper's render flag
	 *
	 * @return int  render flag
	 */
	public function getRenderFlag()
	{
		return $this->_renderFlag;
	}

	// Finder methods:

	/**
	 * Finds all relations (forward and reverse) for the given $page
	 *
	 * The form of the returned array:
	 * <code>
	 * // $page denotes an instance of Zend_Navigation_Page
	 * $returned = array(
	 *     'rel' => array(
	 *         'alternate' => array($page, $page, $page),
	 *         'start'     => array($page),
	 *         'next'      => array($page),
	 *         'prev'      => array($page),
	 *         'canonical' => array($page)
	 *     ),
	 *     'rev' => array(
	 *         'section'   => array($page)
	 *     )
	 * );
	 * </code>
	 *
	 * @param  Zend_Navigation_Page $page  page to find links for
	 * @return array                       related pages
	 */
	public function findAllRelations(Zend_Navigation_Page $page,
	$flag = null)
	{
		if (!is_int($flag)) {
			$flag = self::RENDER_ALL;
		}

		$result = array('rel' => array(), 'rev' => array());
		$native = array_values(self::$_RELATIONS);

		foreach (array_keys($result) as $rel) {
			$meth = 'getDefined' . ucfirst($rel);
			$types = array_merge($native, array_diff($page->$meth(), $native));

			foreach ($types as $type) {
				if (!$relFlag = array_search($type, self::$_RELATIONS)) {
					$relFlag = self::RENDER_CUSTOM;
				}
				if (!($flag & $relFlag)) {
					continue;
				}
				if ($found = $this->findRelation($page, $rel, $type)) {
					if (!is_array($found)) {
						$found = array($found);
					}
					$result[$rel][$type] = $found;
				}
			}
		}

		return $result;
	}

	/**
	 * Finds relations of the given $rel=$type from $page
	 *
	 * This method will first look for relations in the page instance, then
	 * by searching the root container if nothing was found in the page.
	 *
	 * @param  Zend_Navigation_Page $page       page to find relations for
	 * @param  string              $rel         relation, "rel" or "rev"
	 * @param  string              $type        link type, e.g. 'start', 'next'
	 * @return Zend_Navigaiton_Page|array|null  page(s), or null if not found
	 * @throws Zend_View_Exception              if $rel is not "rel" or "rev"
	 */
	public function findRelation(Zend_Navigation_Page $page, $rel, $type)
	{
		if (!in_array($rel, array('rel', 'rev'))) {
			require_once 'Zend/View/Exception.php';
			throw new Zend_View_Exception(sprintf(
                'Invalid argument: $rel must be "rel" or "rev"; "%s" given',
			$rel));
		}

		if (!$result = $this->_findFromProperty($page, $rel, $type)) {
			$result = $this->_findFromSearch($page, $rel, $type);
		}

		return $result;
	}

	/**
	 * Finds relations of given $type for $page by checking if the
	 * relation is specified as a property of $page
	 *
	 * @param  Zend_Navigation_Page $page       page to find relations for
	 * @param  string              $rel         relation, 'rel' or 'rev'
	 * @param  string              $type        link type, e.g. 'start', 'next'
	 * @return Zend_Navigation_Page|array|null  page(s), or null if not found
	 */
	protected function _findFromProperty(Zend_Navigation_Page $page, $rel, $type)
	{
		$method = 'get' . ucfirst($rel);
		if ($result = $page->$method($type)) {
			if ($result = $this->_convertToPages($result)) {
				if (!is_array($result)) {
					$result = array($result);
				}

				foreach ($result as $key => $page) {
					if (!$this->accept($page)) {
						unset($result[$key]);
					}
				}

				return count($result) == 1 ? $result[0] : $result;
			}
		}

		return null;
	}

	/**
	 * Finds relations of given $rel=$type for $page by using the helper to
	 * search for the relation in the root container
	 *
	 * @param  Zend_Navigation_Page $page  page to find relations for
	 * @param  string              $rel    relation, 'rel' or 'rev'
	 * @param  string              $type   link type, e.g. 'start', 'next', etc
	 * @return array|null                  array of pages, or null if not found
	 */
	protected function _findFromSearch(Zend_Navigation_Page $page, $rel, $type)
	{
		$found = null;

		$method = 'search' . ucfirst($rel) . ucfirst($type);
		if (method_exists($this, $method)) {
			$found = $this->$method($page);
		}

		return $found;
	}

	// Search methods:

	/**
	 * Searches the root container for the forward 'start' relation of the given
	 * $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to the first document in a collection of documents. This link type
	 * tells search engines which document is considered by the author to be the
	 * starting point of the collection.
	 *
	 * @param  Zend_Navigation_Page $page  page to find relation for
	 * @return Zend_Navigation_Page|null   page or null
	 */
	public function searchRelStart(Zend_Navigation_Page $page)
	{
		$found = $this->_findRoot($page);
		if (!$found instanceof Zend_Navigation_Page) {
			$found->rewind();
			$found = $found->current();
		}

		if ($found === $page || !$this->accept($found)) {
			$found = null;
		}

		return $found;
	}

	/**
	 * Searches the root container for the forward 'next' relation of the given
	 * $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to the next document in a linear sequence of documents. User
	 * agents may choose to preload the "next" document, to reduce the perceived
	 * load time.
	 *
	 * @param  Zend_Navigation_Page $page  page to find relation for
	 * @return Zend_Navigation_Page|null   page(s) or null
	 */
	public function searchRelNext(Zend_Navigation_Page $page)
	{
		$found = null;
		$break = false;
		$iterator = new RecursiveIteratorIterator($this->_findRoot($page),
		RecursiveIteratorIterator::SELF_FIRST);
		foreach ($iterator as $intermediate) {
			if ($intermediate === $page) {
				// current page; break at next accepted page
				$break = true;
				continue;
			}

			if ($break && $this->accept($intermediate)) {
				$found = $intermediate;
				break;
			}
		}

		return $found;
	}

	/**
	 * Searches the root container for the forward 'prev' relation of the given
	 * $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to the previous document in an ordered series of documents. Some
	 * user agents also support the synonym "Previous".
	 *
	 * @param  Zend_Navigation_Page $page  page to find relation for
	 * @return Zend_Navigation_Page|null   page or null
	 */
	public function searchRelPrev(Zend_Navigation_Page $page)
	{
		$found = null;
		$prev = null;
		$iterator = new RecursiveIteratorIterator(
		$this->_findRoot($page),
		RecursiveIteratorIterator::SELF_FIRST);
		foreach ($iterator as $intermediate) {
			if (!$this->accept($intermediate)) {
				continue;
			}
			if ($intermediate === $page) {
				$found = $prev;
				break;
			}

			$prev = $intermediate;
		}

		return $found;
	}

	/**
	 * Searches the root container for forward 'chapter' relations of the given
	 * $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to a document serving as a chapter in a collection of documents.
	 *
	 * @param  Zend_Navigation_Page $page       page to find relation for
	 * @return Zend_Navigation_Page|array|null  page(s) or null
	 */
	public function searchRelChapter(Zend_Navigation_Page $page)
	{
		$found = array();

		// find first level of pages
		$root = $this->_findRoot($page);

		// find start page(s)
		$start = $this->findRelation($page, 'rel', 'start');
		if (!is_array($start)) {
			$start = array($start);
		}

		foreach ($root as $chapter) {
			// exclude self and start page from chapters
			if ($chapter !== $page &&
			!in_array($chapter, $start) &&
			$this->accept($chapter)) {
				$found[] = $chapter;
			}
		}

		switch (count($found)) {
			case 0:
				return null;
			case 1:
				return $found[0];
			default:
				return $found;
		}
	}

	/**
	 * Searches the root container for forward 'section' relations of the given
	 * $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to a document serving as a section in a collection of documents.
	 *
	 * @param  Zend_Navigation_Page $page       page to find relation for
	 * @return Zend_Navigation_Page|array|null  page(s) or null
	 */
	public function searchRelSection(Zend_Navigation_Page $page)
	{
		$found = array();

		// check if given page has pages and is a chapter page
		if ($page->hasPages() && $this->_findRoot($page)->hasPage($page)) {
			foreach ($page as $section) {
				if ($this->accept($section)) {
					$found[] = $section;
				}
			}
		}

		switch (count($found)) {
			case 0:
				return null;
			case 1:
				return $found[0];
			default:
				return $found;
		}
	}

	/**
	 * Searches the root container for forward 'subsection' relations of the
	 * given $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to a document serving as a subsection in a collection of
	 * documents.
	 *
	 * @param  Zend_Navigation_Page $page       page to find relation for
	 * @return Zend_Navigation_Page|array|null  page(s) or null
	 */
	public function searchRelSubsection(Zend_Navigation_Page $page)
	{
		$found = array();

		if ($page->hasPages()) {
			// given page has child pages, loop chapters
			foreach ($this->_findRoot($page) as $chapter) {
				// is page a section?
				if ($chapter->hasPage($page)) {
					foreach ($page as $subsection) {
						if ($this->accept($subsection)) {
							$found[] = $subsection;
						}
					}
				}
			}
		}

		switch (count($found)) {
			case 0:
				return null;
			case 1:
				return $found[0];
			default:
				return $found;
		}
	}

	/**
	 * Searches the root container for the reverse 'section' relation of the
	 * given $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to a document serving as a section in a collection of documents.
	 *
	 * @param  Zend_Navigation_Page $page  page to find relation for
	 * @return Zend_Navigation_Page|null   page(s) or null
	 */
	public function searchRevSection(Zend_Navigation_Page $page)
	{
		$found = null;

		if ($parent = $page->getParent()) {
			if ($parent instanceof Zend_Navigation_Page &&
			$this->_findRoot($page)->hasPage($parent)) {
				$found = $parent;
			}
		}

		return $found;
	}

	/**
	 * Searches the root container for the reverse 'section' relation of the
	 * given $page
	 *
	 * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
	 * Refers to a document serving as a subsection in a collection of
	 * documents.
	 *
	 * @param  Zend_Navigation_Page $page  page to find relation for
	 * @return Zend_Navigation_Page|null   page(s) or null
	 */
	public function searchRevSubsection(Zend_Navigation_Page $page)
	{
		$found = null;

		if ($parent = $page->getParent()) {
			if ($parent instanceof Zend_Navigation_Page) {
				$root = $this->_findRoot($page);
				foreach ($root as $chapter) {
					if ($chapter->hasPage($parent)) {
						$found = $parent;
						break;
					}
				}
			}
		}

		return $found;
	}

	// Util methods:

	/**
	 * Returns the root container of the given page
	 *
	 * When rendering a container, the render method still store the given
	 * container as the root container, and unset it when done rendering. This
	 * makes sure finder methods will not traverse above the container given
	 * to the render method.
	 *
	 * @param  Zend_Navigaiton_Page $page  page to find root for
	 * @return Zend_Navigation_Container   the root container of the given page
	 */
	protected function _findRoot(Zend_Navigation_Page $page)
	{
		if ($this->_root) {
			return $this->_root;
		}

		$root = $page;

		while ($parent = $page->getParent()) {
			$root = $parent;
			if ($parent instanceof Zend_Navigation_Page) {
				$page = $parent;
			} else {
				break;
			}
		}

		return $root;
	}

	/**
	 * Converts a $mixed value to an array of pages
	 *
	 * @param  mixed $mixed                     mixed value to get page(s) from
	 * @param  bool  $recursive                 whether $value should be looped
	 *                                          if it is an array or a config
	 * @return Zend_Navigation_Page|array|null  empty if unable to convert
	 */
	protected function _convertToPages($mixed, $recursive = true)
	{
		if (is_object($mixed)) {
			if ($mixed instanceof Zend_Navigation_Page) {
				// value is a page instance; return directly
				return $mixed;
			} elseif ($mixed instanceof Zend_Navigation_Container) {
				// value is a container; return pages in it
				$pages = array();
				foreach ($mixed as $page) {
					$pages[] = $page;
				}
				return $pages;
			} elseif ($mixed instanceof Zend_Config) {
				// convert config object to array and extract
				return $this->_convertToPages($mixed->toArray(), $recursive);
			}
		} elseif (is_string($mixed)) {
			// value is a string; make an URI page
			return Zend_Navigation_Page::factory(array(
                'type' => 'uri',
                'uri'  => $mixed
			));
		} elseif (is_array($mixed) && !empty($mixed)) {
			if ($recursive && is_numeric(key($mixed))) {
				// first key is numeric; assume several pages
				$pages = array();
				foreach ($mixed as $value) {
					if ($value = $this->_convertToPages($value, false)) {
						$pages[] = $value;
					}
				}
				return $pages;
			} else {
				// pass array to factory directly
				try {
					$page = Zend_Navigation_Page::factory($mixed);
					return $page;
				} catch (Exception $e) {
				}
			}
		}

		// nothing found
		return null;
	}

	// Render methods:

	/**
	 * Renders the given $page as a link element, with $attrib = $relation
	 *
	 * @param  Zend_Navigation_Page $page      the page to render the link for
	 * @param  string               $attrib    the attribute to use for $type,
	 *                                         either 'rel' or 'rev'
	 * @param  string               $relation  relation type, muse be one of;
	 *                                         alternate, appendix, bookmark,
	 *                                         chapter, contents, copyright,
	 *                                         glossary, help, home, index, next,
	 *                                         prev, section, start, stylesheet,
	 *                                         subsection
	 * @return string                          rendered link element
	 * @throws Zend_View_Exception             if $attrib is invalid
	 */
	public function renderLink(Zend_Navigation_Page $page, $attrib, $relation)
	{
		if (!in_array($attrib, array('rel', 'rev'))) {
			require_once 'Zend/View/Exception.php';
			throw new Zend_View_Exception(sprintf(
                    'Invalid relation attribute "%s", must be "rel" or "rev"',
			$attrib));
		}

		if (!$href = $page->getHref()) {
			return '';
		}

		// TODO: add more attribs
		// http://www.w3.org/TR/html401/struct/links.html#h-12.2
		$attribs = array(
		$attrib  => $relation,
            'href'   => $href,
            'title'  => $page->getLabel()
		);

		return '<link' .
		$this->_htmlAttribs($attribs) .
		$this->getClosingBracket();
	}

	// Zend_View_Helper_Navigation_Helper:

	/**
	 * Renders helper
	 *
	 * Implements {@link Zend_View_Helper_Navigation_Helper::render()}.
	 *
	 * @param  Zend_Navigation_Container $container  [optional] container to
	 *                                               render. Default is to
	 *                                               render the container
	 *                                               registered in the helper.
	 * @return string                                helper output
	 */
	public function render(Zend_Navigation_Container $container = null)
	{
		if (null === $container) {
			$container = $this->getContainer();
		}

		if ($active = $this->findActive($container)) {
			$active = $active['page'];
		} else {
			// no active page
			return '';
		}

		$output = '';
		$indent = $this->getIndent();
		$this->_root = $container;

		$result = $this->findAllRelations($active, $this->getRenderFlag());
		foreach ($result as $attrib => $types) {
			foreach ($types as $relation => $pages) {
				foreach ($pages as $page) {
					if ($r = $this->renderLink($page, $attrib, $relation)) {
						$output .= $indent . $r . self::EOL;
					}
				}
			}
		}

		$this->_root = null;

		// return output (trim last newline by spec)
		return strlen($output) ? rtrim($output, self::EOL) : '';
	}
}
